package gaboli.common.utils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.Scanner;

/**
 * This code contains convenience methods for
 * encryption and salt generation. Enhanced with methods from from
 * http://www.rgagnon.com/javadetails/java-0400.html
 *
 * @author C. Burrell (deltafront@gmail.com)
 */
public class EncryptionUtils
{
    public static final String AES = "AES";
    private static char[] chars = new char[]{ '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'q', 'w', 'e', 'r',
            't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n',
            'm', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z',
            'X', 'C', 'V', 'B', 'N', 'M' };

    /**
     * Generates random salt for encryption.
     *
     * @param seasoning string upon which salt is based.
     * @return salted string which is in plain text and URL-safe (alphanumeric).
     * @since 1.0
     */
    public String randomSalt( String seasoning )
    {
        return randomSalt( seasoning, true );
    }

    /**
     * Generates random salt for encryption.
     *
     * @param seasoning  string upon which salt is based.
     * @param plain_text whether or not the result should be in plain - text (alphanumeric).
     * @return salted string.
     * @since 1.0
     */
    public String randomSalt( String seasoning, boolean plain_text )
    {
        String out = "";
        Random random = new Random();
        for( int i = 0; i < seasoning.length(); i++ )
        {
            int position = random.nextInt( chars.length );
            out += chars[ position ];
            if( ( position > 0 ) && ( i % position == 0 ) )
            {
                position = random.nextInt( chars.length );
                out += chars[ position ];
            }
            if( !plain_text )
            {
                out = oneWayEncrypt( out, "MD5" );
            }
        }
        return out;
    }

    /**
     * Encrypts a password and stores the key in a file. If the file object does not exist, it will be generated.
     *
     * @param plain_text_string text string to be encrypted.
     * @param keyFile  File that is to contain the key.
     * @return encrypted password.
     * @since 1.0
     */
    public String encrypt( String plain_text_string, File keyFile )
    {
        String out = null;
        try
        {
            if( !keyFile.exists() )
            {
                KeyGenerator keyGen = KeyGenerator.getInstance( AES );
                keyGen.init( 128 );
                SecretKey sk = keyGen.generateKey();
                FileWriter fw = new FileWriter( keyFile );
                fw.write( byteArrayToHexString( sk.getEncoded() ) );
                fw.flush();
                fw.close();
            }

            SecretKeySpec sks = getSecretKeySpec( keyFile );
            Cipher cipher = Cipher.getInstance( AES );
            cipher.init( Cipher.ENCRYPT_MODE, sks, cipher.getParameters() );
            byte[] encrypted = cipher.doFinal( plain_text_string.getBytes() );
            out = byteArrayToHexString( encrypted );
        }
        catch( Exception e )
        {
            e.printStackTrace( System.err );
        }
        return out;

    }

    /**
     * Decrypts the encrypted string using the keyfile.
     *
     * @param encrypted_string String to be decrypted.
     * @param keyFile File that contains the key.
     * @return Decrypted string.
     * @since 1.0
     */
    public String decrypt( String encrypted_string, File keyFile )
    {
        String out = null;
        try
        {
            SecretKeySpec sks = getSecretKeySpec( keyFile );
            Cipher cipher = Cipher.getInstance( AES );
            cipher.init( Cipher.DECRYPT_MODE, sks );
            byte[] decrypted = cipher.doFinal( hexStringToByteArray( encrypted_string ) );
            out = new String( decrypted );
        }
        catch( Exception e )
        {
            e.printStackTrace( System.err );
        }

        return out;
    }

    /**
     * Encrypts string using supplied algorithm name.
     *
     * @param string  String to be encrypted.
     * @param algoritm Hashing algorith to be used. Valid algs are MD2, MD5, SHA-1, SHA-256, SHA-384, and SHA-512.
     * @return encrypted string, or null if error was encountered.
     * @since 1.0
     */
    public static String oneWayEncrypt( String string, String algoritm )
    {
        String out = null;
        java.security.MessageDigest digest = null;
        try
        {
            digest = java.security.MessageDigest.getInstance( algoritm );
            digest.reset();
            digest.update( string.getBytes() );
            out = new String( digest.digest() );
        }
        catch( NoSuchAlgorithmException e )
        {
            e.printStackTrace();
        }
        return out;
    }

    /**
     * Encrypts string using SHA-1 algorithm.
     *
     * @param string String to be encrypted.
     * @return encrypted string, or null if error was encountered.
     * @since 1.0
     */
    public String oneWayEncrypt( String string )
    {
        return oneWayEncrypt( string, "SHA-1" );
    }

    private SecretKeySpec getSecretKeySpec( File keyFile ) throws NoSuchAlgorithmException, IOException
    {
        byte[] key = readKeyFile( keyFile );
        SecretKeySpec sks = new SecretKeySpec( key, AES );
        return sks;
    }

    private byte[] readKeyFile( File keyFile ) throws FileNotFoundException
    {
        Scanner scanner = new Scanner( keyFile ).useDelimiter( "\\Z" );
        String keyValue = scanner.next();
        scanner.close();
        return hexStringToByteArray( keyValue );
    }

    private String byteArrayToHexString( byte[] b )
    {
        StringBuffer sb = new StringBuffer( b.length * 2 );
        for( int i = 0; i < b.length; i++ )
        {
            int v = b[ i ] & 0xff;
            if( v < 16 )
            {
                sb.append( '0' );
            }
            sb.append( Integer.toHexString( v ) );
        }
        return sb.toString().toUpperCase();
    }

    private byte[] hexStringToByteArray( String s )
    {
        byte[] b = new byte[ s.length() / 2 ];
        for( int i = 0; i < b.length; i++ )
        {
            int index = i * 2;
            int v = Integer.parseInt( s.substring( index, index + 2 ), 16 );
            b[ i ] = ( byte ) v;
        }
        return b;
    }

}
